/**
 * Copyright (c) 2011-2023, James Zhan 詹波 (jfinal@126.com).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.jse.tpl;

import java.io.File;
import java.lang.reflect.Method;
import java.math.RoundingMode;
import java.util.HashMap;
import java.util.Map;

import com.jse.Io;
import com.jse.Jse;
import com.jse.Lang;
import com.jse.Strings;

/**
 * Engine
 * 
 * Example：
 * Engine.use().getTemplate(fileName).render(...);
 * Engine.use().getTemplate(fileName).renderToString(...);
 */
public class Tpl {
	
	public static final Map<String,Template> SQLS = new HashMap<>();
	private static Tpl MAIN_ENGINE;
	private static Map<String, Tpl> engineMap = new HashMap<String, Tpl>(64, 0.5F);
	/**
	 * 设置 true 为开发模式，支持模板文件热加载
	 * 设置 false 为生产模式，不支持模板文件热加载，以达到更高的性能
	 */
	public static boolean dev = true;
	
	// Create main engine
	static {
		MAIN_ENGINE = new Tpl("main");
		engineMap.put("main", MAIN_ENGINE);
	}
	
	private String name;
	
	private EngineConfig config = new EngineConfig();
	
	private Map<String, Template> templateCache = new HashMap<String, Template>(2048, 0.5F);
	
	/**
	 * Create engine by engineName without management of JFinal 
	 */
	public Tpl(String engineName) {
		this.name = engineName;
	}
	
	/**
	 * 切换引擎
	 */
	public static Tpl use(String engineName) {
		return engineMap.get(engineName);
	}
	public static Tpl use() {
		return MAIN_ENGINE;
	}
	
	public Object eval(String expr, Map<?, ?> data) {
		if(data==null)data=new HashMap<>();
		String stringTemplate = "#(" + expr + ")";
		return getTemplateByString(stringTemplate,false).renderToString(data);
	}
	
	/**
	 * 创建引擎
	 */
	public synchronized static Tpl create(String engineName) {
		if (Strings.isBlank(engineName)) {
			throw new IllegalArgumentException("Engine name can not be blank");
		}
		engineName = engineName.trim();
		if (engineMap.containsKey(engineName)) {
			throw new IllegalArgumentException("Engine already exists : " + engineName);
		}
		Tpl newEngine = new Tpl(engineName);
		engineMap.put(engineName, newEngine);
		return newEngine;
	}
	
	public Template getTemplate(String filePath, boolean cache) {
		File file=new File(filePath);
		if (!cache) {
			return buildTemplateBySource(new ISource(file, cache));
		}
		
		String cacheKey = Lang.md5(file);
		Template template = templateCache.get(cacheKey);
		if (template == null) {
			template = buildTemplateBySource(new ISource(file, cacheKey));
			templateCache.put(cacheKey, template);
		} else if (dev) {
			if (template.isModified()) {
				template = buildTemplateBySource(new ISource(file, cacheKey));
				templateCache.put(cacheKey, template);
			}
		}
		return template;
	}
	public Template getTemplateByClass(String cacheKey) {
		String file=Io.read(Jse.clazz.getResourceAsStream(cacheKey));
		Template template = templateCache.get(cacheKey);
		if (template == null) {
			template = buildTemplateBySource(new ISource(file, cacheKey));
			templateCache.put(cacheKey, template);
		} else if (dev) {
			if (template.isModified()) {
				template = buildTemplateBySource(new ISource(file, cacheKey));
				templateCache.put(cacheKey, template);
			}
		}
		return template;
	}
	/**
	 * Get template by string content
	 * 
	 * 重要：StringSource 中的 cacheKey = HashKit.md5(content)，也即 cacheKey
	 *     与 content 有紧密的对应关系，当 content 发生变化时 cacheKey 值也相应变化
	 *     因此，原先 cacheKey 所对应的 Template 缓存对象已无法被获取，当 getTemplateByString(String)
	 *     的 String 参数的数量不确定时会引发内存泄漏
	 *     
	 *     当 getTemplateByString(String, boolean) 中的 String 参数的
	 *     数量可控并且确定时，才可对其使用缓存 
	 *     
	 * @param content 模板内容
	 * @param cache true 则缓存 Template，否则不缓存
	 */
	public Template getTemplateByString(String content, boolean cache) {
		if (!cache) {
			return buildTemplateBySource(new ISource(content, cache));
		}
		
		String cacheKey = Lang.md5(content);
		Template template = templateCache.get(cacheKey);
		if (template == null) {
			template = buildTemplateBySource(new ISource(content, cacheKey));
			templateCache.put(cacheKey, template);
		} else if (dev) {
			if (template.isModified()) {
				template = buildTemplateBySource(new ISource(content, cacheKey));
				templateCache.put(cacheKey, template);
			}
		}
		return template;
	}
	
	private Template buildTemplateBySource(ISource source) {
		Env env = new Env(config);
		Parser parser = new Parser(env, source.getContent(), source.getId());
		if (dev) {
			env.addSource(source);
		}
		Stat stat = parser.parse();
		Template template = new Template(env, stat);
		return template;
	}
	
	/**
	 * Add shared function by file
	 */
	public Tpl addSharedFunction(String fileName) {
		config.addSharedFunction(fileName);
		return this;
	}
	
	/**
	 * Add shared function by ISource
	 */
	public Tpl addSharedFunction(ISource source) {
		config.addSharedFunction(source);
		return this;
	}
	
	/**
	 * Add shared function by files
	 */
	public Tpl addSharedFunction(String... fileNames) {
		config.addSharedFunction(fileNames);
		return this;
	}
	
	/**
	 * Add shared function by string content
	 */
	public Tpl addSharedFunctionByString(String content) {
		config.addSharedFunctionByString(content);
		return this;
	}
	
	/**
	 * Add shared object
	 */
	public Tpl addSharedObject(String name, Object object) {
		config.addSharedObject(name, object);
		return this;
	}
	
	public Tpl removeSharedObject(String name) {
		config.removeSharedObject(name);
		return this;
	}
	
	/**
	 * 添加枚举类型，便于在模板中使用
	 * 
	 * <pre>
	 * 例子：
	 * 1：定义枚举类型
	 * public enum UserType {
	 * 
	 *   ADMIN,
	 *   USER;
	 *   
	 *   public String hello() {
	 *      return "hello";
	 *   }
	 * }
	 * 
	 * 2：配置
	 * engine.addEnum(UserType.class);
	 * 
	 * 3：模板中使用
	 * ### 以下的对象 u 通过 Controller 中的 setAttr("u", UserType.ADMIN) 传递
	 * #if( u == UserType.ADMIN )
	 *    #(UserType.ADMIN)
	 *    
	 *    #(UserType.ADMIN.name())
	 *    
	 *    #(UserType.ADMIN.hello())
	 * #end
	 * 
	 * </pre>
	 */
	public Tpl addEnum(Class<? extends Enum<?>> enumClass) {
		Map<String, Enum<?>> map = new java.util.LinkedHashMap<>();
		Enum<?>[] es = enumClass.getEnumConstants();
		for (Enum<?> e : es) {
			map.put(e.name(), e);
		}
		return addSharedObject(enumClass.getSimpleName(), map);
	}
	
	/**
	 * Add shared method from object
	 */
	public Tpl addSharedMethod(Object sharedMethodFromObject) {
		config.addSharedMethod(sharedMethodFromObject);
		return this;
	}
	
	/**
	 * Add shared method from class
	 */
	public Tpl addSharedMethod(Class<?> sharedMethodFromClass) {
		config.addSharedMethod(sharedMethodFromClass);
		return this;
	}
	
	/**
	 * Add shared static method of Class
	 */
	public Tpl addSharedStaticMethod(Class<?> sharedStaticMethodFromClass) {
		config.addSharedStaticMethod(sharedStaticMethodFromClass);
		return this;
	}
	
	/**
	 * Remove shared Method by method name
	 */
	public Tpl removeSharedMethod(String methodName) {
		config.removeSharedMethod(methodName);
		return this;
	}
	
	/**
	 * Remove shared Method of the Class
	 */
	public Tpl removeSharedMethod(Class<?> clazz) {
		config.removeSharedMethod(clazz);
		return this;
	}
	
	/**
	 * Remove shared Method
	 */
	public Tpl removeSharedMethod(Method method) {
		config.removeSharedMethod(method);
		return this;
	}
	
	/**
	 * Remove template cache by cache key
	 */
	public void removeTemplateCache(String cacheKey) {
		templateCache.remove(cacheKey);
	}
	
	/**
	 * Remove all template cache
	 */
	public void removeAllTemplateCache() {
		templateCache.clear();
	}
	
	public int getTemplateCacheSize() {
		return templateCache.size();
	}
	
	public String getName() {
		return name;
	}
	
	public String toString() {
		return "Template Engine: " + name;
	}
	
	// Engine config below ---------
	
	public EngineConfig  getEngineConfig() {
		return config;
	}
	
	public Tpl setDatePattern(String datePattern) {
		config.setDatePattern(datePattern);
		return this;
	}
	
	public String getDatePattern() {
		return config.getDatePattern();
	}
	
	/**
	 * 设置 #number 指令与 Arith 中浮点数的舍入规则，默认为 RoundingMode.HALF_UP "四舍五入"
	 */
	public Tpl setRoundingMode(RoundingMode roundingMode) {
		config.setRoundingMode(roundingMode);
		return this;
	}
	
	public Tpl setBufferSize(int bufferSize) {
		config.setBufferSize(bufferSize);
		return this;
	}
	
	public Tpl setReentrantBufferSize(int reentrantBufferSize) {
		config.setReentrantBufferSize(reentrantBufferSize);
		return this;
	}
	
	public static void addExtensionMethod(Class<?> targetClass, Object objectOfExtensionClass) {
		MethodKit.addExtensionMethod(targetClass, objectOfExtensionClass);
	}
	
	public static void addExtensionMethod(Class<?> targetClass, Class<?> extensionClass) {
		MethodKit.addExtensionMethod(targetClass, extensionClass);
	}
	
	public static void removeExtensionMethod(Class<?> targetClass, Object objectOfExtensionClass) {
		MethodKit.removeExtensionMethod(targetClass, objectOfExtensionClass);
	}
	
	public static void removeExtensionMethod(Class<?> targetClass, Class<?> extensionClass) {
		MethodKit.removeExtensionMethod(targetClass, extensionClass);
	}
	/**
	 * 设置为 true 支持表达式、变量名、方法名、模板函数名使用中文
	 */
	public static void setChineseExpression(boolean enable) {
		CharTable.setChineseExpression(enable);
	}
}





